package com.huangkailong.gitimgbed.jgit;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import lombok.Data;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jgit.api.*;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.*;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;

import java.io.*;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

/**
 * @author huangkailong
 * @since 1.0.0
 */
@Getter
@Slf4j
public class Jgit {
    private final String GIT_URI_SUFFIX = ".git";
    /**
     * GIT 仓库地址
     */
    private final String url;
    /**
     * GIT 仓库
     */
    private final String branch;
    /**
     * GIT 仓库根目录名称
     */
    private String dirName;
    /**
     * GIT 仓库所在路径
     */
    private final String localPath;
    /**
     * 认证
     */
    private CredentialsProvider credentialsProvider;

    private TransportConfigCallback transportConfigCallback;



    public Jgit(String url, String branch, String localPath) {
        this.url = url;
        this.branch = branch;
        this.localPath = localPath;
    }

    public Jgit(String url, String branch, String localPath, CredentialsProvider credentialsProvider) {
        this.url = url;
        this.branch = branch;
        this.localPath = localPath;
        this.credentialsProvider = credentialsProvider;
    }

    public Jgit(String url, String branch, String localPath, TransportConfigCallback transportConfigCallback) {
        this.url = url;
        this.branch = branch;
        this.localPath = localPath;
        this.transportConfigCallback = transportConfigCallback;
    }

    /**
     * 获取 git 仓库本部目录的文件夹名称
     *
     * @return rootDir
     */
    public String getDirName(){
        if(StrUtil.isNotBlank(dirName)) {
            return dirName;
        }

        int i = url.lastIndexOf("/");
        if(i != -1){
            int endIndex = url.lastIndexOf(GIT_URI_SUFFIX);
            if(endIndex != -1) {
                return (dirName = url.substring(i+1, endIndex));
            }
        }
        throw new IllegalArgumentException("dirName 为空且无法通过 Git 仓库地址推断出 dirName，当前配置的 Git 地址为："+ url);
    }

    /**
     * git clone
     *
     * @throws GitAPIException
     */
    public void cloneRepository() throws GitAPIException{
        final File directory = new File(localPath, getDirName());
        final CloneCommand cloneCommand = Git.cloneRepository()
                .setURI(url)
                .setBranch(branch)
                .setDirectory(directory);
        setAuth(cloneCommand);
        try (Git git = cloneCommand.call()){}
    }

    /**
     * git pull
     *
     * @return {@link PullResult}
     * @throws IOException
     * @throws GitAPIException
     */
    public PullResult pull() throws IOException, GitAPIException {
        try (Git git = Git.open(new File(localPath, getDirName()))){
            final PullCommand pull = git.pull();
            setAuth(pull);
            return pull.call();
        }
    }

    /**
     * git add .
     *
     * @return
     * @throws IOException
     * @throws GitAPIException
     */
    public DirCache addAll() throws IOException, GitAPIException {
        try (Git git = Git.open(new File(localPath, getDirName()))){
            return git.add().addFilepattern(".").call();
        }
    }

    /**
     * git commit
     *
     * @param message 提交信息
     * @return
     * @throws IOException
     * @throws GitAPIException
     */
    public RevCommit commit(String message) throws IOException, GitAPIException {
        try (Git git = Git.open(new File(localPath, getDirName()))){
            return git.commit().setMessage(message).call();
        }
    }

    /**
     *
     * @return
     * @throws IOException
     * @throws GitAPIException
     */
    public Iterable<PushResult> push() throws IOException, GitAPIException {
        try (Git git = Git.open(new File(localPath, getDirName()))){
            final PushCommand pushCommand = git.push();
            setAuth(pushCommand);
            return pushCommand.call();
        }
    }

    /**
     * 比较 treeId 和 otherTreeId 的异同。
     *
     * @param oldTreeId {@link AnyObjectId}
     * @param newTreeId {@link AnyObjectId}
     * @return {@link DiffEntry} List
     * @throws IOException
     */
    public List<DiffEntry> diff(AnyObjectId oldTreeId, AnyObjectId newTreeId) throws IOException {
        try (Git git = Git.open(new File(localPath, getDirName()))){
            Repository master = git.getRepository();
            ObjectReader objectReader = master.newObjectReader();
            CanonicalTreeParser oldCanonicalTreeParser = new CanonicalTreeParser(null, objectReader, oldTreeId);
            CanonicalTreeParser newCanonicalTreeParser = new CanonicalTreeParser(null, objectReader, newTreeId);
            DiffFormatter diffFormatter = new DiffFormatter(new ByteArrayOutputStream());
            diffFormatter.setRepository(master);
            diffFormatter.setDetectRenames(true);
            return diffFormatter.scan(oldCanonicalTreeParser, newCanonicalTreeParser);
        }
    }

    /**
     * @see Jgit#diff(AnyObjectId, AnyObjectId)
     */
    public List<DiffEntry> diff(RevCommit oldRevCommit, RevCommit newRecCommit) throws IOException{
        ObjectId oldTreeId = oldRevCommit.getTree().getId();
        ObjectId newTreeId = newRecCommit.getTree().getId();
        if(oldTreeId.equals(newTreeId)){
            log.debug("传入的 oldTreeId 和 newTreeId 一致，oldTreeId:{}, newTreeId:{}", oldTreeId, newTreeId);
            return Collections.emptyList();
        }
        return diff(oldTreeId, newTreeId);
    }

    /**
     * 对比最近两次 commit 异同, 等价于 git diff HEAD~1
     */
    public List<DiffEntry> diffRecently() throws IOException, GitAPIException {
        try (Git git = Git.open(new File(localPath, getDirName()))){
            Iterable<RevCommit> revCommits = git.log().call();
            List<RevCommit> commits = StreamSupport.stream(revCommits.spliterator(), false)
                    .sorted(Comparator.comparing(r -> ((RevCommit)r).getCommitTime()).reversed())
                    .collect(Collectors.toList());

            if(commits.size() < 2) {
                return Collections.emptyList();
            }

            return diff(commits.get(1), commits.get(0));
        }
    }

    /**
     * 获取最新的 {@link RevCommit}
     *
     * @return 没有则返回 null
     */
    public RevCommit getLastRevCommit() throws IOException, GitAPIException {
        try (Git git = Git.open(new File(localPath, getDirName()))){
            Iterable<RevCommit> revCommits = git.log().call();
            Iterator<RevCommit> iterator = revCommits.iterator();
            if(iterator.hasNext()) {
                return iterator.next();
            }
            return null;
        }
    }

    /**
     * 检查本地仓库是否存在。
     *
     * @return true 表示存在
     */
    public boolean exits(){
        try (Git git = Git.open(new File(localPath, getDirName()))){
        } catch (IOException e) {
            return false;
        }
        return true;
    }

    /**
     * 获取 GIT 仓库路径。
     *
     * @return {@link File}
     */
    public File getRepositoryPath(){
        return new File(localPath, getDirName());
    }

    /**
     * 获取指定文件的时间信息。
     *
     * @param path 文件路径，相对路径，相对于 RootDir，且必须是路径分隔符必须是 /
     * @return {@link FileTimeInfo}
     */
    public FileTimeInfo getFileTimeInfo(String path) throws GitAPIException, IOException {
        try (Git git = Git.open(new File(this.localPath, getDirName()))){
            final Iterable<RevCommit> revCommitIterable = git.log().addPath(path).call();
            final List<RevCommit> revCommitList = StreamSupport.stream(revCommitIterable.spliterator(), false)
                    .sorted(Comparator.comparing(RevCommit::getCommitTime)).collect(Collectors.toList());
            if(CollectionUtil.isEmpty(revCommitList)){
                return null;
            }
            final int createTimeSecond = revCommitList.get(0).getCommitTime();
            final LocalDateTime createTime = LocalDateTime.ofEpochSecond(createTimeSecond, 0, ZoneOffset.ofHours(8));
            final int updateTimeSecond = revCommitList.get(revCommitList.size() - 1).getCommitTime();
            final LocalDateTime updateTime = LocalDateTime.ofEpochSecond(updateTimeSecond, 0, ZoneOffset.ofHours(8));
            return new FileTimeInfo(updateTime, createTime);
        }
    }


    /**
     * 批量执行 git 命令
     *
     * @param callback
     * @throws IOException
     */
    public void executeCommands(JGitCallback callback) throws Exception {
        try (Git git = Git.open(new File(this.localPath, getDirName()))){
            callback.execute(git, this);
        }
    }


    public void setAuth(TransportCommand<?, ?> transportCommand){
        if(Objects.nonNull(transportConfigCallback)){
            transportCommand.setTransportConfigCallback(transportConfigCallback);
            return;
        }
        if(Objects.nonNull(credentialsProvider)){
            transportCommand.setCredentialsProvider(credentialsProvider);
        }
    }


    @Data
    public static class FileTimeInfo {
        /**
         * 最近的更新时间
         */
        private LocalDateTime updateTime;
        /**
         * 创建时间
         */
        private LocalDateTime createTime;

        public FileTimeInfo(LocalDateTime updateTime, LocalDateTime createTime) {
            this.updateTime = updateTime;
            this.createTime = createTime;
        }
    }

    public static class Builder{
        /**
         * GIT 仓库地址
         */
        private String url;
        /**
         * GIT 仓库
         */
        private String branch;
        /**
         * GIT 仓库根目录名称
         */
        private String dirName;
        /**
         * GIT 仓库所在路径
         */
        private String localPath;
        /**
         * {@link CredentialsProvider}
         */
        private CredentialsProvider credentialsProvider;

        private TransportConfigCallback transportConfigCallback;

        public String getUrl() {
            return url;
        }

        public Builder setUrl(String url) {
            this.url = url;
            return this;
        }

        public String getBranch() {
            return branch;
        }

        public Builder setBranch(String branch) {
            this.branch = branch;
            return this;
        }

        public String getDirName() {
            return dirName;
        }

        public Builder setDirName(String dirName) {
            this.dirName = dirName;
            return this;
        }

        public String getLocalPath() {
            return localPath;
        }

        public Builder setLocalPath(String localPath) {
            this.localPath = localPath;
            return this;
        }

        public CredentialsProvider getCredentialsProvider() {
            return credentialsProvider;
        }

        public Builder setCredentialsProvider(CredentialsProvider credentialsProvider) {
            this.credentialsProvider = credentialsProvider;
            return this;
        }

        public TransportConfigCallback getTransportConfigCallback() {
            return transportConfigCallback;
        }

        public Builder setTransportConfigCallback(TransportConfigCallback transportConfigCallback) {
            this.transportConfigCallback = transportConfigCallback;
            return this;
        }

        public Jgit build(){
            if(StrUtil.isBlank(url) || StrUtil.isBlank(branch) || StrUtil.isBlank(localPath)){
                throw new IllegalArgumentException("url, branch 和 localPath 均不能为空!");
            }
            if(Objects.nonNull(transportConfigCallback)){
                return new Jgit(url, branch, localPath, transportConfigCallback);
            }else if(url.startsWith("git")) {
                return new Jgit(url, branch, localPath, transport -> {
                    SshTransport sshTransport = (SshTransport) transport;
                    sshTransport.setSshSessionFactory(new MyJschConfigSessionFactory());
                });
            }

            if(Objects.nonNull(credentialsProvider)){
                return new Jgit(url, branch, localPath, credentialsProvider);
            }

            return new Jgit(url, branch, localPath);
        }
    }
}
